package data;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import ai.human.PlayerGroup;
import data.Data.PlayerState;
import data.Data.Race;
import data.Data.Role;
import data.Data.Skill;

/**
 * Represents a player in the game, from a data perspective.
 * 
 * @author Julien
 * 
 */
public class Player extends Position implements Serializable {
	// Player info
	private String m_name;
	private PlayerType m_type;
	private int m_number;

	public String name() {
		return m_name;
	}

	public PlayerType type() {
		return m_type;
	}

	public Race race() {
		return m_type.m_race;
	}

	public Role role() {
		return m_type.m_role;
	}

	public int number() {
		return m_number;
	}

	// player state during a match
	PlayerState m_state;

	public PlayerState state() {
		return m_state;
	}

	public void setState(PlayerState state) {
		m_state = state;
	}

	/** Returns true if the player is selected, false otherwise */
	public boolean isSelected() {
		return m_isSelected;
	}

	public void select(boolean selected) {
		m_isSelected = selected;
	}

	private boolean m_isSelected;

	public boolean hasActed() {
		return m_hasActed;
	}

	public void setHasActed(boolean hasActed) {
		m_hasActed = hasActed;
	}

	private boolean m_hasActed;

	public boolean hasBlitzed() {
		return m_hasBlitzed;
	}

	public void setHasBlitzed(boolean hasBlitzed) {
		m_hasBlitzed = hasBlitzed;
	}

	private boolean m_hasBlitzed;

	private boolean m_hasBall;

	public boolean hasBall() {
		return m_hasBall;
	}

	public void setHasBall(boolean hasBall) {
		m_hasBall = hasBall;
	}

	private boolean m_hasUsedDodge;

	public boolean hasUsedDodge() {
		return m_hasUsedDodge;
	}

	public void hasUsedDodge(boolean hasUsed) {
		m_hasUsedDodge = hasUsed;
	}

	private int m_stunnedOnTurn = -100;

	public int wasStunnedOnTurn() {
		return m_stunnedOnTurn;
	}

	public void setWasStunnedOnTurn(int turn) {
		m_stunnedOnTurn = turn;
	}

	// Non-game related info
	private int m_experience;
	private int m_level;

	public int XP() {
		return m_experience;
	}

	public int level() {
		return m_level;
	}

	// current statistics
	private int m_currentMovement;

	public int currentMA() {
		return m_currentMovement;
	}

	public void resetMA() {
		m_currentMovement = m_movementAllowance;
	}

	public void setCurrentMA(int ma) {
		m_currentMovement = ma;
	}

	// Player statistics
	private int m_movementAllowance;
	private int m_strength;
	private int m_agility;
	private int m_armourValue;
	private Set<Skill> m_skills = new HashSet<Skill>();

	public int MA() {
		return m_movementAllowance;
	}

	public int ST() {
		return m_strength;
	}

	public int AG() {
		return m_agility;
	}

	public int AV() {
		return m_armourValue;
	}

	public boolean hasSkill(Skill skill) {
		return m_skills.contains(skill);
	}

	public Set<Skill> skills() {
		return Collections.unmodifiableSet(m_skills);
	}

	public void setMA(int MA) {
		m_movementAllowance = MA;
	}

	public void setST(int ST) {
		m_strength = ST;
	}

	public void setAG(int AG) {
		m_agility = AG;
	}

	public void setAV(int AV) {
		m_armourValue = AV;
	}

	public void addSkill(Skill skill) {
		m_skills.add(skill);
	}

	public void removeSkill(Skill skill) {
		m_skills.remove(skill);
	}

	public void removeAllSkills() {
		m_skills.clear();
	}

	public void setXP(int XP) {
		m_experience = XP;
	}

	public void setLevel(int level) {
		m_level = level;
	}

	public void setName(String name) {
		m_name = name;
	}

	public void setNumber(int number) {
		m_number = number;
	}

	/**
	 * Player group.
	 */
	private PlayerGroup playerGroup;

	final static private DefaultPlayers m_defaultPlayers = new DefaultPlayers();

	/**
	 * Creates a player from a document node.
	 * 
	 * @param node
	 */
	public Player(Element node) {
		load(node);
		m_state = PlayerState.NONE;
	}

	/**
	 * Creates a player with its default attributes & skills, according to Race
	 * & Role.
	 * 
	 * @param name
	 * @param race
	 * @param role
	 * @param number
	 */
	public Player(String name, Race race, Role role, int number) {
		m_name = name;
		m_type = new PlayerType(race, role);
		m_number = number;
		m_state = PlayerState.NONE;

		m_level = 1;
		m_experience = 0;

		Player defaultPlayer = m_defaultPlayers.get(m_type);

		setMA(defaultPlayer.MA());
		setST(defaultPlayer.ST());
		setAG(defaultPlayer.AG());
		setAV(defaultPlayer.AV());
		m_skills.addAll(defaultPlayer.m_skills);

		resetMA();
	}

	/**
	 * Creates a player without using the default values, but using the
	 * prescribed parameters
	 * 
	 * @param name
	 * @param race
	 * @param role
	 * @param number
	 * @param MA
	 * @param ST
	 * @param AG
	 * @param AV
	 */
	public Player(String name, Race race, Role role, int number, int MA, int ST, int AG, int AV, Set<Skill> skills) {
		m_name = name;
		m_type = new PlayerType(race, role);
		m_number = number;
		m_state = PlayerState.NONE;
		m_level = 1;
		m_experience = 0;
		setMA(MA);
		setST(ST);
		setAG(AG);
		setAV(AV);

		if (skills != null)
			m_skills.addAll(skills);

		resetMA();
	}

	/**
	 * Setter for the "playerGroup" property.
	 * 
	 * @param playerGroup
	 *            the playerGroup to set
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public void setPlayerGroup(PlayerGroup playerGroup) {
		this.playerGroup = playerGroup;
	}

	/**
	 * Getter for the "playerGroup" property.
	 * 
	 * @return the playerGroup
	 * 
	 * @author Tomasz Dąbrowski
	 */
	public PlayerGroup getPlayerGroup() {
		return playerGroup;
	}

	/**
	 * Saves the player to the given DOM document, under the parent node.
	 * 
	 * @param doc
	 *            The document to save the player to.
	 * @param parent
	 *            The parent node in the XML hierarchy that the player needs to
	 *            be saved to.
	 */
	public void save(Document doc, Element parent) {
		// Create player node in the parent element
		Element node = doc.createElement("Player");
		parent.appendChild(node);

		// Save player info
		node.setAttribute("Name", name());
		node.setAttribute("Race", race().toString());
		node.setAttribute("Role", role().toString());
		node.setAttribute("Number", Integer.toString(number()));

		node.setAttribute("XP", Integer.toString(XP()));
		node.setAttribute("Level", Integer.toString(level()));

		// Save all player data
		node.setAttribute("MA", Integer.toString(MA()));
		node.setAttribute("ST", Integer.toString(ST()));
		node.setAttribute("AG", Integer.toString(AG()));
		node.setAttribute("AV", Integer.toString(AV()));

		// Save skills
		for (Skill skill : m_skills) {
			Element skillNode = doc.createElement("Skill");
			node.appendChild(skillNode);

			skillNode.setAttribute("Name", skill.toString());
		}
	}

	/**
	 * Loads the player data from the given XML Element.
	 * 
	 * @param node
	 *            The node to load the player from.
	 */
	public void load(Element node) {
		m_name = node.getAttribute("Name");

		Race race = Race.valueOf(node.getAttribute("Race"));
		Role role = Role.valueOf(node.getAttribute("Role"));
		m_type = new PlayerType(race, role);

		m_number = Integer.parseInt(node.getAttribute("Number"));

		m_experience = Integer.parseInt(node.getAttribute("XP"));
		m_level = Integer.parseInt(node.getAttribute("Level"));

		m_movementAllowance = Integer.parseInt(node.getAttribute("MA"));
		m_strength = Integer.parseInt(node.getAttribute("ST"));
		m_agility = Integer.parseInt(node.getAttribute("AG"));
		m_armourValue = Integer.parseInt(node.getAttribute("AV"));

		m_skills = new HashSet<Skill>();
		NodeList skillList = node.getElementsByTagName("Skill");
		for (int n = 0; n < skillList.getLength(); n++) {
			Node skillNode = skillList.item(n);
			if (skillNode.getNodeType() != Node.ELEMENT_NODE)
				continue;

			Element skill = (Element) skillNode;
			m_skills.add(Skill.valueOf(skill.getAttribute("Name")));
		}

		resetMA();
	}

	public boolean equals(Object o) {
		if (this == o)
			return true;

		if (!(o instanceof Player))
			return false;

		Player other = (Player) o;

		return (m_name.equals(other.m_name) && m_type.equals(other.m_type) && m_number == other.m_number && level() == other.level()
				&& XP() == other.XP() && MA() == other.MA() && ST() == other.ST() && AG() == other.AG() && AV() == other.AV() && m_skills
				.equals(other.m_skills));
	}

	/**
	 * Returns a hash value of the player
	 * 
	 * @return hash value
	 */
	public int hashCode() {
		int hash = 17;
		hash = 37 * hash + m_name.hashCode();
		hash = 37 * hash + m_type.hashCode();
		hash = 37 * hash + m_number;
		return hash;
	}

	/**
	 * Returns a one-letter string representing the player Role.
	 * 
	 * @return one-letter code
	 */
	public String typeString() {
		switch (role()) {
		case BLITZER:
			return "Z";
		case BLOCKER:
			return "B";
		case THROWER:
			return "T";
		case LINEMAN:
			return "L";
		case CATCHER:
			return "C";
		}

		return "";
	}

	/**
	 * Returns a string representation of the player, needed for correct display
	 * in the combo boxes
	 * 
	 * @return Player string representation
	 */
	public String toString() {
		return m_name + " " + typeString() + " #" + number();
	}

	/**
	 * Returns the minimum XP for the player to level up.
	 * 
	 * @return XP needed (total) to go up the next level.
	 */
	public int nextLevelXP() {
		if (m_level >= 7)
			return Integer.MAX_VALUE;

		int levels[] = { 0, 6, 16, 31, 51, 76, 176 };
		return levels[m_level];
	}

	/**
	 * Checks whether a player can level up, based on his current level and XP.
	 * 
	 * @return True if the player can level up, false otherwise.
	 */
	public boolean canLevelUp() {
		return m_experience >= nextLevelXP();
	}

	/**
	 * Checks if the player is "over-leveled" compared to its XP.
	 * 
	 * @return True if the player has not the correct XP to be this level, false
	 *         otherwise.
	 */
	public boolean canLevelDown() {
		if (m_level <= 1)
			return false;

		int level = Math.max(m_level, 7);

		int levels[] = { 0, 6, 16, 31, 51, 76, 176 };
		return m_experience < levels[level - 1];
	}
}

/**
 * This class holds the "default" players for each race & role, making it easy
 * to use in tests, default team creation, comparisons, etc.
 * 
 * @author Julien
 * 
 */
class DefaultPlayers {
	Map<PlayerType, Player> m_defaultPlayers = new HashMap<PlayerType, Player>();

	DefaultPlayers() {
		// Note that in this method, additional brackets will be used to clarify
		// scope of variables, and prevent unintentional reuse of variable
		// names.

		// These skills are valid for human and orc players
		{
			HashSet<Skill> blitzerSkills = new HashSet<Skill>();
			blitzerSkills.add(Skill.BLOCK);

			HashSet<Skill> catcherSkills = new HashSet<Skill>();
			catcherSkills.add(Skill.CATCH);
			catcherSkills.add(Skill.DODGE);

			HashSet<Skill> throwerSkills = new HashSet<Skill>();
			throwerSkills.add(Skill.SURE_HANDS);
			throwerSkills.add(Skill.PASS);

			// Default human players
			Player hBlitzer = new Player("", Race.HUMAN, Role.BLITZER, -1, 7, 3, 3, 8, blitzerSkills);
			m_defaultPlayers.put(new PlayerType(hBlitzer.race(), hBlitzer.role()), hBlitzer);

			Player hCatcher = new Player("", Race.HUMAN, Role.CATCHER, -1, 8, 2, 3, 7, catcherSkills);
			m_defaultPlayers.put(new PlayerType(hCatcher.race(), hCatcher.role()), hCatcher);

			Player hLineman = new Player("", Race.HUMAN, Role.LINEMAN, -1, 6, 3, 3, 8, null);
			m_defaultPlayers.put(new PlayerType(hLineman.race(), hLineman.role()), hLineman);

			Player hThrower = new Player("", Race.HUMAN, Role.THROWER, -1, 6, 3, 3, 8, throwerSkills);
			m_defaultPlayers.put(new PlayerType(hThrower.race(), hThrower.role()), hThrower);

			// Default orc players
			Player oBlitzer = new Player("", Race.ORC, Role.BLITZER, -1, 6, 3, 3, 9, blitzerSkills);
			m_defaultPlayers.put(new PlayerType(oBlitzer.race(), oBlitzer.role()), oBlitzer);

			Player oBlocker = new Player("", Race.ORC, Role.BLOCKER, -1, 4, 4, 2, 9, null);
			m_defaultPlayers.put(new PlayerType(oBlocker.race(), oBlocker.role()), oBlocker);

			Player oLineman = new Player("", Race.ORC, Role.LINEMAN, -1, 5, 3, 3, 9, null);
			m_defaultPlayers.put(new PlayerType(oLineman.race(), oLineman.role()), oLineman);

			Player oThrower = new Player("", Race.ORC, Role.THROWER, -1, 5, 3, 3, 8, throwerSkills);
			m_defaultPlayers.put(new PlayerType(oThrower.race(), oThrower.role()), oThrower);
		}

		// These skills are valid only for elf players!
		{
			HashSet<Skill> elfBlitzerSkills = new HashSet<Skill>();
			elfBlitzerSkills.add(Skill.BLOCK);
			elfBlitzerSkills.add(Skill.SIDE_STEP);

			HashSet<Skill> elfCatcherSkills = new HashSet<Skill>();
			elfCatcherSkills.add(Skill.CATCH);
			elfCatcherSkills.add(Skill.NERVES_OF_STEEL);

			HashSet<Skill> elfThrowerSkills = new HashSet<Skill>();
			elfThrowerSkills.add(Skill.PASS);

			// Default elf players
			Player eBlitzer = new Player("", Race.ELF, Role.BLITZER, -1, 7, 3, 4, 8, elfBlitzerSkills);
			m_defaultPlayers.put(new PlayerType(eBlitzer.race(), eBlitzer.role()), eBlitzer);

			Player eCatcher = new Player("", Race.ELF, Role.CATCHER, -1, 8, 3, 4, 7, elfCatcherSkills);
			m_defaultPlayers.put(new PlayerType(eCatcher.race(), eCatcher.role()), eCatcher);

			Player eLineman = new Player("", Race.ELF, Role.LINEMAN, -1, 6, 3, 4, 7, null);
			m_defaultPlayers.put(new PlayerType(eLineman.race(), eLineman.role()), eLineman);

			Player eThrower = new Player("", Race.ELF, Role.THROWER, -1, 6, 3, 4, 7, elfThrowerSkills);
			m_defaultPlayers.put(new PlayerType(eThrower.race(), eThrower.role()), eThrower);
		}
	}

	/**
	 * Returns the default player associated with the player type (i.e. race +
	 * role)
	 * 
	 * @param playerType
	 *            the player type wanted (race + role)
	 * @return the default player that fills in this type.
	 */
	Player get(PlayerType playerType) {
		if (m_defaultPlayers.containsKey(playerType))
			return m_defaultPlayers.get(playerType);
		else {
			Player defaultPlayer = new Player("", playerType.m_race, playerType.m_role, -1, 1, 1, 1, 1, null);
			return defaultPlayer;
		}
	}
}